﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using System.Linq;
using System.Dynamic;

using X = Microsoft.Xna.Framework;
using XCP = Microsoft.Xna.Framework.Content.Pipeline;
using XCPG = Microsoft.Xna.Framework.Content.Pipeline.Graphics;

using XNA.BLEND.dom;

namespace XNA.BLEND.parser {
  internal class BlendParser {
    internal XCPG.NodeContent Parse( string path, XCP.ContentImporterContext context, XCP.ContentIdentity id ) {
      System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

      this.ImporterContext = context;
      this.MainID = id;

      this.LogDelimiter();
      this.Log( "parsing file [{0}]", new object[] { path } );

      this.Document = new Document();
      this.Document.Load( path );

      this.RootContent = new XCPG.NodeContent();
      this.RootContent.Name = System.IO.Path.GetFileNameWithoutExtension( path ); ;

      XmlDocument xml = this.Document.Transform2Xml();

      if ( context != null ) {
        string xmlfilepath = context.IntermediateDirectory + System.IO.Path.GetFileName( path ) + @".dump.xml";
        xml.Save( xmlfilepath );
        this.Log( "dumped xml representation of blend file [" + xmlfilepath + "]" );
      }
      this.ParseScene();

      sw.Stop();
      this.Log( " time elapsed: {0}", new object[] { sw.Elapsed.ToString() } );
      this.LogDelimiter();

      return this.RootContent;
    }

    private void ParseScene() {
      dynamic elemScene = this.Document.SceneBlock.Scene;
      this.Log( "parsing scene [" + this.ParseBlendName( elemScene.id.name ) + "]" );

      dynamic r = elemScene.r;
      float frs_sec = (short)r.frs_sec;
      float frs_sec_base = (float)r.frs_sec_base;
      this.AnimationFPS = frs_sec / frs_sec_base;

      dynamic elemBase = this.Document.FileBlocks[(UIntPtr)elemScene.@base.first].Base;
      while ( elemBase != null ) {
        dynamic elemObject = this.Document.FileBlocks[(UIntPtr)elemBase.@object].Object;

        this.Log( "parsing object [" + this.ParseBlendName( elemObject.id.name ) + "]" );

        int type = (short)elemObject.type;
        if ( type == 1 ) { // Mesh
          dynamic elemMesh = this.Document.FileBlocks[(UIntPtr)elemObject.data].Mesh;
          dynamic elemAnim = this.Document.FileBlocks[(UIntPtr)elemObject.adt].AnimData;
          if ( elemAnim == null ) {
            elemAnim = this.Document.FileBlocks[(UIntPtr)elemMesh.adt].AnimData;
          }

          dynamic elemDefGrpRoot = null;
          UIntPtr dbfirst = (UIntPtr)elemObject.defbase.first;
          if ( dbfirst != UIntPtr.Zero ) {
            elemDefGrpRoot = this.Document.FileBlocks[dbfirst].bDeformGroup;
          }

          this.ParseMesh( elemMesh, elemAnim, elemDefGrpRoot );
        }
        else if ( type == 10 ) { // Lamp
        }
        else if ( type == 11 ) { // Camera
        }
        else if ( type == 25 ) { // Armature
          dynamic elemArma = this.Document.FileBlocks[(UIntPtr)elemObject.data].bArmature;

          this.ParseArmature( elemArma );
        }
        else {
          this.Log( "Unknown Object-type {0} in {1}!", new object[] { type, this.MainID.SourceFilename } );
        }

        UIntPtr next = (UIntPtr)elemBase.next;
        if ( next != UIntPtr.Zero ) {
          elemBase = this.Document.FileBlocks[(UIntPtr)elemBase.next].Base;
        }
        else {
          elemBase = null;
        }
      }
    }

    private void ParseMesh( dynamic elemMesh, dynamic elemAnim, dynamic elemDefGrpRoot ) {
      if ( elemMesh != null ) {
        XCPG.MeshBuilder mb = XCPG.MeshBuilder.StartMesh( this.ParseBlendName( elemMesh.id.name ) );
        this.Log( "parsing mesh [" + mb.Name + "]" );
        
        // validate
        dynamic polys = this.Document.FileBlocks[elemMesh.mpoly].MPoly;
        foreach ( dynamic poly in polys ) {
          if ( poly.totloop != 3 ) {
            throw new Exception(
              "Only Trianglelist-Polygons are supportet! Triangulate your model first (Ctrl-T in Edit-Mode)!"
            );
          }
        }

        this.ParseMaterial( mb, elemMesh );
        this.ParseVertexData( mb, elemMesh, elemDefGrpRoot );

        XCPG.MeshContent mc = mb.FinishMesh();

        this.RootContent.Children.Add( mc );
      }   
    }

    private void ParseMaterial( XCPG.MeshBuilder mb, dynamic me ) {
      XCPG.MaterialContent mc = null;

      UIntPtr ptrMatSrc = (UIntPtr)me.mat;
      UIntPtr ptrMatDst = UIntPtr.Zero;

      if ( ptrMatSrc != UIntPtr.Zero ) {
        ptrMatDst = (UIntPtr)this.Document.FileBlocks[ptrMatSrc].Link.Pointer;
      }

      if ( ptrMatDst != UIntPtr.Zero ) {
        dynamic mat = this.Document.FileBlocks[ptrMatDst].Material;

        XCPG.BasicMaterialContent matc = new XCPG.BasicMaterialContent();
        matc.Name = this.ParseBlendName( mat.id.name );
        matc.Identity = this.MainID;
        matc.DiffuseColor = mat.sss_col;
        matc.EmissiveColor = X.Vector3.Zero;
        matc.Alpha = mat.alpha;
        matc.SpecularColor = X.Vector3.Zero;

        dynamic mtex = this.Document.FileBlocks[mat.mtex[0]].MTex;
        dynamic tex = this.Document.FileBlocks[mtex.tex].Tex;
        dynamic ima = this.Document.FileBlocks[tex.ima].Image;

        string texpath = ima.name;
        texpath = 
          System.IO.Path.GetFullPath(
            System.IO.Path.Combine( new string [] {
              System.IO.Path.GetDirectoryName( this.MainID.SourceFilename ), 
              texpath.Replace( @"//", @"" ) }
            )
          );
        XCP.ExternalReference<XCPG.TextureContent> extref = new XCP.ExternalReference<XCPG.TextureContent>();
        extref.Filename = texpath;
        matc.Textures.Add( "Texture", extref );
        matc.Texture.OpaqueData.Add( "TextureCoordinate", "TextureCoordinate0" );
        mc = matc;
      }

      if ( mc != null ) {
        mb.SetMaterial( mc );
      }
      else {
        this.Log( "Mesh [{0}] has no valid Material!", new object[] { mb.Name } );
      }
    }

    private List<string> ParseDeformGroups( dynamic dgr ) {
      List<string> r = new List<string>();

      if ( dgr != null ) {
        do {
          r.Add( dgr.name );
          UIntPtr nextdgr = (UIntPtr)dgr.next;
          if ( nextdgr != UIntPtr.Zero ) {
            dgr = this.Document.FileBlocks[nextdgr].bDeformGroup;
          }
          else {
            dgr = null;
          }
        } while ( dgr != null );

      }

      return r;
    }

    private List<XCPG.BoneWeightCollection> GetWeights( dynamic me, List<string> dgnames ) {
      List<XCPG.BoneWeightCollection> r = new List<XCPG.BoneWeightCollection>();

      dynamic dverts = this.Document.FileBlocks[me.dvert].MDeformVert;

      foreach ( dynamic dvert in dverts ) {
        dynamic weightelems = this.Document.FileBlocks[dvert.dw].MDeformWeight;

        XCPG.BoneWeightCollection w = new XCPG.BoneWeightCollection();

        if ( weightelems is ExpandoObject ) {
          int deformidx = weightelems.def_nr;
          float weight = weightelems.weight;
          if ( weight > 1.0f ) {
            weight = 1.0f;
          }

          w.Add( new XCPG.BoneWeight(
            dgnames[deformidx],
            weight
          ) );
        }
        else {
          foreach ( dynamic weightelem in weightelems ) {
            int deformidx = weightelem.def_nr;
            float weight = weightelem.weight;
            if ( weight > 1.0f ) {
              weight = 1.0f;
            }

            w.Add( new XCPG.BoneWeight(
              dgnames[deformidx],
              weight
            ) );
          }
        }

        r.Add( w );
      }

      return r;
    }

    private void ParseArmature( dynamic ar ) {
      XCPG.BoneContent bc = new XCPG.BoneContent();
      bc.Name = this.ParseBlendName( ar.id.name );
      this.Log( "parsing armature [{0}]", new object[] { bc.Name } );
      bc.Transform = X.Matrix.Identity;

      UIntPtr ptrFirstBone = (UIntPtr)ar.bonebase.first;
      dynamic bone = this.Document.FileBlocks[ptrFirstBone].Bone;
      if (bone != null) {
        bc.Transform = X.Matrix.Identity;

        foreach ( XCPG.BoneContent child in this.ParseChildBones( bone ) ) {
          bc.Children.Add( child );
        }
      }

      // TODO: find out what action-groups are good for? maybe local transforms
      //XmlNodeList ActionNodes = this.Document.FindFileBlocks( "AC", "bAction" );

      var elemsAction = 
        from a in this.Document.FileBlocks.Values        
        where a.__Code == "AC"
        select ((dynamic)(a)).bAction;

      foreach ( dynamic elemAction in elemsAction ) {
        this.ParseAction( elemAction, bc );
      }

      this.RootContent.Children.Add( bc );
    }

    private void ParseAction( dynamic elmAction, XCPG.NodeContent parent ) {
      XCPG.AnimationContent ac = new XCPG.AnimationContent();

      ac.Name = this.ParseBlendName( elmAction.id.name );
      this.Log( "parsing action [" + ac.Name + "]" );
      
      List<string> bones = new List<string>();
      Dictionary<string, string> channelMap = new Dictionary<string, string>();
      Dictionary<ActionProp, Dictionary<string, Dictionary<int, float[]>>> propFramesListMap =
                                new Dictionary<ActionProp,Dictionary<string, Dictionary<int, float[]>>>();
      propFramesListMap.Add( ActionProp.LOC, new Dictionary<String, Dictionary<int, float[]>>() );
      propFramesListMap.Add( ActionProp.ROT_QUAT, new Dictionary<String, Dictionary<int, float[]>>() );
      propFramesListMap.Add( ActionProp.SCALE, new Dictionary<String, Dictionary<int, float[]>>() );

      dynamic elmFCurve = this.Document.FileBlocks[elmAction.curves.first].FCurve;
      while ( elmFCurve != null ) {
        string rnapath = this.Document.FileBlocks[elmFCurve.rna_path].Link.Expression;

        if ( rnapath != string.Empty ) {
          int startidx = rnapath.IndexOf("[\"");
          int endidx = rnapath.IndexOf("\"]");
          if ( startidx >= 0 && endidx > startidx ) {
            string bonename = rnapath.Substring( startidx + 2, endidx - startidx - 2 );
            if ( !bones.Contains( bonename ) ) {
              bones.Add( bonename );
            }

            dynamic elmBezTs = this.Document.FileBlocks[elmFCurve.bezt].BezTriple;

            foreach ( dynamic elmBezT in elmBezTs ) {
              this.ParseBezierTriplet( propFramesListMap, ActionValExtensions.from( this, elmFCurve ), bonename, elmBezT );
            }
          }

          UIntPtr next = elmFCurve.next;
          if ( next != UIntPtr.Zero ) {
            elmFCurve = this.Document.FileBlocks[next].FCurve;
          }
          else {
            elmFCurve = null;
          }
        }
      }

      // validate property types support
      foreach ( ActionProp p in propFramesListMap.Keys ) {
          switch ( p ) {
            case ActionProp.LOC:
              continue;
            case ActionProp.ROT_QUAT:
              continue;
            case ActionProp.SCALE:
              continue;
            case ActionProp.ROT_EUL:
              throw new Exception("Euler rotations currently not supportet!");
            case ActionProp.ROT_AXIS:
              throw new Exception("Axis rotations currently not supportet!");
          }
          throw new Exception( "Unsupported property! " );
      }

      Dictionary<string, Dictionary<int, ActionKeyframe>> channelKeyframes = new Dictionary<string, Dictionary<int, ActionKeyframe>>();

      foreach ( string currentBone in bones ) {
        Dictionary<int, ActionKeyframe> keyframes = new Dictionary<int, ActionKeyframe>();
        channelKeyframes.Add( currentBone, keyframes );

        Dictionary<string, Dictionary<int, float[]>> propFrames = null;
        propFrames = propFramesListMap[ActionProp.LOC];
        if (propFrames != null) {
          if ( propFrames.ContainsKey( currentBone ) ) {
            foreach ( KeyValuePair<int, float[]> kvp in propFrames[currentBone] ) {
              ActionKeyframe keyframe = new ActionKeyframe();
              keyframe.Location = kvp.Value;
              keyframes.Add( kvp.Key, keyframe );
            }
          }
        }
        propFrames = propFramesListMap[ActionProp.ROT_QUAT];
        if (propFrames != null) {
          if ( propFrames.ContainsKey( currentBone ) ) {
            foreach ( KeyValuePair<int, float[]> kvp in propFrames[currentBone] ) {
              ActionKeyframe keyframe = null;
              if ( keyframes.ContainsKey( kvp.Key ) ) {
                keyframe = keyframes[kvp.Key];
              }
              else {
                keyframe = new ActionKeyframe();
                keyframes.Add( kvp.Key, keyframe );
              }

              keyframe.Rotation = kvp.Value;
            }
          }
        }
        propFrames = propFramesListMap[ActionProp.SCALE];
        if (propFrames != null) {
          if ( propFrames.ContainsKey( currentBone ) ) {
            foreach ( KeyValuePair<int, float[]> kvp in propFrames[currentBone] ) {
              ActionKeyframe keyframe = null;
              if ( keyframes.ContainsKey( kvp.Key ) ) {
                keyframe = keyframes[kvp.Key];
              }
              else {
                keyframe = new ActionKeyframe();
                keyframes.Add( kvp.Key, keyframe );
              }

              keyframe.Scale = kvp.Value;
            }
          }
        }
      }

      TimeSpan duration = TimeSpan.Zero;

      foreach ( string channel in channelKeyframes.Keys ) {
        XCPG.AnimationChannel ach = new XCPG.AnimationChannel();
        
        Dictionary<int, ActionKeyframe> actkeyframes = channelKeyframes[channel];

        foreach ( int framenum in actkeyframes.Keys ) {
          ActionKeyframe akf = actkeyframes[framenum];
          X.Matrix matrixFrame = akf.Matrix * this.BoneMatrix[channel];

          TimeSpan currentTS = TimeSpan.FromSeconds( framenum / this.AnimationFPS );

          ach.Add( new XCPG.AnimationKeyframe( currentTS, matrixFrame ) );

          if ( currentTS > duration ) {
            duration = currentTS;
          }
        }

        // TODO: find correct end frame for exact duration calculation
        ac.Duration = duration;

        ac.Channels.Add( channel, ach );
      }

      if ( duration > TimeSpan.Zero && ac.Channels.Count > 0 ) {
        parent.Animations.Add( ac.Name, ac );
      }
    }

    private void ParseVertexData( XCPG.MeshBuilder mb, dynamic me, dynamic dgr ) {
      // lookup deformgroup-names
      List<string> dgnames = this.ParseDeformGroups( dgr );

      // construct weights
      List<XCPG.BoneWeightCollection> weights = this.GetWeights( me, dgnames );

      // construct channels
      int chWeights = -1;
      if ( dgnames.Count > 0 ) {
        chWeights = mb.CreateVertexChannel<XCPG.BoneWeightCollection>( XCPG.VertexChannelNames.Weights() );
      }
      int chNormal = mb.CreateVertexChannel<X.Vector3>( XCPG.VertexChannelNames.Normal() );
      int chTexmap = mb.CreateVertexChannel<X.Vector2>( XCPG.VertexChannelNames.TextureCoordinate( 0 ) );


      dynamic verts = this.Document.FileBlocks[me.mvert].MVert;
      dynamic loops = this.Document.FileBlocks[me.mloop].MLoop;
      dynamic loopUVs = this.Document.FileBlocks[me.mloopuv].MLoopUV;

      List<X.Vector3> positions = new List<X.Vector3>();
      List<uint> indizes = new List<uint>();

      List<X.Vector3> normals = new List<X.Vector3>();
      List<X.Vector2> texcoords = new List<X.Vector2>();

      int[] positionMap = new int[verts.Count];
      int[] normalMap = new int[verts.Count];

      int i = 0;
      foreach ( dynamic vert in verts ) {
        X.Vector3 p = vert.co;
        X.Vector3 n = vert.no;
        n.Normalize();

        positions.Add( p );
        normals.Add( n );

        positionMap[i] = mb.CreatePosition( p );
        i++;
      }

      i = 0;
      mb.SwapWindingOrder = true;
      foreach ( dynamic loop in loops ) {
        dynamic loopUV = loopUVs[i];
        int vidx = loop.v;

        if ( chWeights >= 0 ) {
          mb.SetVertexChannelData( chWeights, weights[vidx] );
        }

        mb.SetVertexChannelData( chNormal, normals[vidx] );
        X.Vector2 texvec = loopUV.uv;
        texvec.Y = 1.0f - texvec.Y;
        mb.SetVertexChannelData( chTexmap, texvec );
        mb.AddTriangleVertex( positionMap[vidx] );
        i++;
      }
    }

    private void ParseBezierTriplet( Dictionary<ActionProp, Dictionary<string, Dictionary<int, float[]>>> propFramesListMap, ActionVal av, string bonename, dynamic bezt ) {
      Dictionary<string, Dictionary<int, float[]>> propFrames = null;
      ActionValAttribute avattr = av.GetAttr();
      if ( avattr != null ) {
        propFrames = propFramesListMap[avattr.ActionProp];
        if ( propFrames == null ) {
          propFrames = new Dictionary<string, Dictionary<int, float[]>>();
          propFramesListMap[av.GetAttr().ActionProp] = propFrames;
        }

        Dictionary<int, float[]> boneFrames = null;
        if ( propFrames.ContainsKey( bonename ) ) {
          boneFrames = propFrames[bonename];
        }

        if ( boneFrames == null ) {
          boneFrames = new Dictionary<int, float[]>();
          propFrames[bonename] = boneFrames;
        }

        float framenumval = bezt.vec.M21;

        int framenum = (int)framenumval;

        if ( Math.Abs( framenumval - framenum ) > 0.3 ) {
          throw new Exception( "BezTriple frame index too far from the nearest integer!!!" );
        }

        if ( framenum < 0 ) {
          throw new Exception( "Animation keyframe before animation start!!!" );
        }

        float[] frame = null;

        if ( boneFrames.ContainsKey( framenum ) ) {
          frame = boneFrames[framenum];
        }

        if ( frame == null ) {
          frame = new float[avattr.ActionProp.GetAttr().Size];
          boneFrames[framenum] = frame;
        }

        frame[avattr.ArrayIndex] = bezt.vec.M22;
      }
    }

    private List<XCPG.BoneContent> ParseChildBones( dynamic bone ) {
      List<XCPG.BoneContent> result = new List<XCPG.BoneContent>();

      XCPG.BoneContent current = new XCPG.BoneContent();
      current.Name = bone.name;

      this.Log( "parsing bone [{0}]", new object[] { current.Name } );

      X.Matrix bone_mat = bone.bone_mat;
      bone_mat.M41 = bone.head.X;
      bone_mat.M42 = bone.head.Y;
      bone_mat.M43 = bone.head.Z;

      if ( bone.parent != UIntPtr.Zero ) {
        dynamic parentbone = this.Document.FileBlocks[bone.parent].Bone;
        this.ParentBone.Add( current.Name, parentbone.name );
        bone_mat.M42 += parentbone.length;
      }
      else {
        this.ParentBone.Add( current.Name, "" );
      }

      current.Transform = bone_mat;

      this.BoneMatrix.Add( current.Name, current.Transform );

      result.Add( current );

      UIntPtr ptrFirstChild = (UIntPtr)bone.childbase.first;
      if ( ptrFirstChild != UIntPtr.Zero ) {
        dynamic childbone = this.Document.FileBlocks[ptrFirstChild].Bone;
        foreach ( XCPG.BoneContent child in this.ParseChildBones( childbone ) ) {
          current.Children.Add( child );
        }
      }

      UIntPtr ptrSibling = (UIntPtr)bone.next;
      if ( ptrSibling != UIntPtr.Zero ) {
        dynamic siblingbone = this.Document.FileBlocks[ptrSibling].Bone;
        foreach ( XCPG.BoneContent sibling in this.ParseChildBones( siblingbone ) ) {
          result.Add( sibling );
        }
      }

      return result;
    }

    private void Log( string logtext ) {
      if ( this.ImporterContext != null ) {
        this.ImporterContext.Logger.LogImportantMessage( "\tXNA.Blend - " + logtext );
      }
    }

    private void LogDelimiter() {
      if ( this.ImporterContext != null ) {
        this.ImporterContext.Logger.LogImportantMessage( "\t---------------------------------------------------------------------------------------" );
      }
    }

    private void Log( string logtext, object[] pars ) {
      if ( this.ImporterContext != null ) {
        this.ImporterContext.Logger.LogImportantMessage( "\tXNA.Blend - " + logtext, pars );
      }
    }

    private string ParseBlendName( string namefield ) {
      return namefield.Substring( 2 );
    }

    XCPG.NodeContent RootContent;

    Dictionary<string, X.Matrix> BoneMatrix = new Dictionary<string, X.Matrix>();
    Dictionary<string, string> ParentBone = new Dictionary<string, string>();

    float AnimationFPS = 24;

    XCP.ContentImporterContext ImporterContext;
    XCP.ContentIdentity MainID;

    internal Document Document;
  }
}
